<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <title>Sucha's Blog - Archive for July, 2021</title>
    <meta name="generator" content="MarkdownProjectCompositor.lua">
    <meta name="author" content="Sucha">
    <meta name="keywords" content="suchang, programming, Linux, Lua">
    <meta name="description" content="Sucha's blog">
    <link rel="shortcut icon" href="../images/ico.png">
    <link rel="stylesheet" type="text/css" href="../styles/blog.css">
    <link rel="stylesheet" type="text/css" href="../styles/prism.min.css">
    <style id="site_theme"></style>
  </head>
  <body>
    <div id="body">
      <div id="text">
	   <!-- Page published by cmark-gfm begins here --><h1>Sucha's Blog ~ Archive for July, 2021</h1>
<p><a id="p1"></a></p>
<div class="date">21年7月17日 周六 21:10</div>
<h2>定制 UINavigationBar 的转场动画</h2>
<p>最近 app UI 改版，交互设计师给出了 UINavigationController 下 Push／Pop 的转场动画，Push 的时候，fromView 会被缓慢往左推，toView 往左慢慢全部盖住 fromView，盖住之前，fromView 大概往左侧移动了 1/3 个页面；Pop 的时候则相反。</p>
<p>单单这样的转场动画是容易实现的，网上的教程一大堆，但难点在 UINavigationBar 也需要实现同样的交互。问题是 UINavigationBar 的动画是系统提供的，UIViewController 仅仅提供左、右、中的 CustomView，而且就我们的 App 来说，首页是隐藏的，在二级页面才会显示出来。由于 UINavigationBar 是系统管理的，转场动画的框架是没有对 UINavigationBar 提供支持的，得自己做。</p>
<p>网上搜索了一下，美团的技术博客提供了思路，具体我用的方案，在 iOS 14 模拟器上验证了蛮久，加上一些调试，如下：</p>
<p>Push 时，对 fromView 的 UINavigationBar 截图；如果是 hidden 状态则不用截图。toView 的 NavigationBar 是假的，但却是用真的 UINavigationBarItem 来构建的，真的 UINavigationBar，系统还未开始绘制呢。假的 NavigationBar 使用了 UINavigationBarItem 的 leftBarItem、rightBarItem 和 titleView 或者 title string。</p>
<p>这里需要注意将 fromViewController 上面对真的 UINavigationBarItem 的链接进行切断，比如 titleView 给他设置一个空的 UIView，leftBarItems、rightBarItems 给设置空的数组来填充。</p>
<p>fromView 截图的 UINavigationBar 按照转场动画的时长 Push 到左侧，toView 带的假的 NavigationBar 则从右侧 Push 进来，等动画结束，将 UINavigationBarItem 上的数据再拿给 fromViewController 使用，将假的 NavigationBar removeFromSuperview。</p>
<p>Pop 的时候需要区分是按返回按钮的 Pop，还是全局右划可中断的 interactive Pop。</p>
<p>先说简单的按返回按钮的 Pop，fromView 在对 UINavigationBar 截图后往右，toView 带着截图的 NavigationBar 出来，等动画结束，截图 NavigationBar removeFromSuperview 就好，这里比较简单。</p>
<p>可中断的 interactive Pop 复杂一些，举例我们用的是 <a href="https://github.com/forkingdog/FDFullscreenPopGesture">FDFullscreenPopGesture</a>，它其实是截获系统提供的全局右划返回功能。但系统的全局右划返回，当 UINavigationBar 的 backItem 被设置为 nil 等时候，是不起作用的。这个库是拿到系统手势检测 view，添加手势，检测到后调用系统的全局右划返回接口，发起是可中断的右划返回动画。</p>
<p>这个库暴露出来的检测手势 gesture 实例，我们后面会用到。</p>
<p>可中断的全局右划返回 Pop 跟直接调用 UINavigationController 的 popViewController 很不一样，由于动画可以中断，因此需要在 fromView 上建立假的 NavigationBar，截图就好。</p>
<p>手势的开始，来自于监听上面说到的 gesture 的 target action，可以监听 begin、change、end 事件，并计算右滑的百分比，用于调整截图 NavigationBar 跟随 fromView。难点来了，手势结束的时候，实际上右划动画也许结束，也许会中断返回，具体会如何，其实是系统判定的，我们并不知道后面动画要怎么样的，包括时长多久也都不知道，因此这里，需要建立一个跟踪机制，我用的是 DisplayLink 来做的，模拟器上看，比较跟手，但是太极端的情况，也会偶尔拉跨。</p>
<p>另外上面说到的假的 NavigationBar 将 UINavigationBarItem 给真的 UINavigationBar 时，其实会闪一下；以及截图 NavigationBar 的时机，实际上比转场动画要早，也就是截图代码不能放在转场动画开始的地方，那个地方才截图的话，太晚了。</p>
<p>因此需要在 UINavigationController 的 push、pop 接口地方做截图，另外全局右划的可中断 pop，截图是在手势开始的 begin，因为流程是 begin 开始后，才会调用到 UINavigationController 的 pop。</p>
<p>太多细节细节，不现场调试根本说不清楚，也不晓得会不会在后面的系统更新中变化，这里大体说了思路，以及需要注意的地方。</p>
<p>当然上面也提到过最后的效果，其实蛮不错的，</p>
<p>说一下前提，如果 UINavigationBar 设置了 translant 且真的用上了半透的话，效果应该是不行的，因为系统真实的 UINavigationBar 动画在底下会动，得实际遮住才行，因此这套东西，我是觉得只能用在非 translant 的场景下，当然，如果只是设置了 translant，但实际上又带了 opaque 的背景，仅仅是为了带一丢丢高斯模糊背景的话，是没问题的。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2021-07.html#p1">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<p><a id="p0"></a></p>
<div class="date">21年7月11日 周日 12:08</div>
<h2>PinStackView</h2>
<p>目前工程主要使用 AutoLayout，但一些复杂页面上面，先不说各种约束优先级的问题，单单就因为约束不对造成的约束失败，在 debug console 里面打印一大堆，都不好定位在哪里，别说去修复了。</p>
<p>总觉得大部分的场景，其实使用固定 frame 的 layout 就足够了，不需要那么复杂，于是我在工程里面引进了 <a href="../blog/2021-03.html#p1">PinLayout</a>。</p>
<p>因为使用 PinLayout 构建复杂页面确实太辛苦，其只用于搭建不复杂的元素，之前使用的是 StackViewLayout，但测试反馈问题比较多，我自己也发现，不大稳定，偶尔还有 crash，毕竟这个库是未完成的状态，然后还自带内存泄漏。</p>
<p>于是就想着自己写一个，毕竟 StackViewLayout 也用了这么久，接口已经蛮熟悉的了，而在功能上面，需要的又不多。</p>
<p>于是就有了 <a href="https://gitee.com/lalawue/PinStackView">PinStackView</a>，这个库相比 PinStackLayout 来说，简单很多，其实只有一个文件，另外，其处理流程上也是很简单的。</p>
<h3>配置</h3>
<ul>
<li>区分主轴 axis 和垂直于主轴的 cross axis，axis 可以为 horizontal 或者 vertical</li>
<li>cross axis 用于配置 alignment，可以是 start、center、end、stretch</li>
<li>区分沿主轴长度是自动增长的为 auto style，否则为 fixed style，默认是 fixed style</li>
<li>接着是 axis 上的分布 distribution，可以是 start、end、equal，其中 equal 对应这 UIStackView 的 distribution equal，每个 item 平均分布</li>
<li>spacing 是 item 之间的间距</li>
<li>padding 之后才是 layout 的内容区域</li>
</ul>
<p>最后增加了一个 autoSizeChangedCallback 的回调函数，如果 style 设置为了 auto，PinStackView 在 axis 上的长度由其 item 动态长度计算得到，每次对 bounds 有更改，都会 callback 通知。</p>
<p>另外上面的设置其实隐含了一些前提，在 fixed style 时，PinStackView 的大小是外部设置的，此时才有 start、end、equal 这些 distribution；而在 auto style 下，只有 start distribution，其实很好理解。</p>
<h3>管理接口</h3>
<p>下面是管理 view 的接口，添加、插入都会返回 PinStackItemInfo</p>
<ul>
<li>addItem() 添加管理 view，返回 PinStackItemInfo，这个接口独立于 addSubview()，通过 addSubview() 添加的，没有绑定 itemInfo，不会自动 layout</li>
<li>insertItem() 用于插入管理 view，同样返回 PinStackItemInfo</li>
<li>removeItem() 删除管理 view，同时从 subviews 里面删除</li>
<li>markDirty()，其实就是 setNeedsLayout()</li>
</ul>
<h3>item 配置</h3>
<p>对于每个管理 view，在添加、插入的时候，都绑定了一个 PinStackItemInfo 用来描述其 layout 信息，下面的接口都可以进行链式配置</p>
<ul>
<li>top()、bottom()、left()、right() 用于配置 margin，在非 equal distribution 时，这些 margin 会和 spacing 累加，可以实现其中的某个元素间距不同；在 equal 时，可以调整其位置</li>
<li>width()、height()、size() 用于配置大小，其中 size() 优先级最高，其次是 width() 和 height()</li>
<li>alignSelf()，独立于总的 cross axis 的配置，可以单独设置某个 view 的 alignment</li>
<li>grow()，当设置为 fixed style 的时候，这个值可以对剩余空间进行分配；同样的，如果剩余空间不足，这个值也用于 shrink</li>
</ul>
<p>上面第二点，如果设置了 alignment 或者 alignSelf 为 stretch，会覆盖 item 在 cross axis 的长度</p>
<h3>计算流程</h3>
<p>建议看代码吧，其实没啥好说的，fxied style 下面，equal 就是平均 axis 的长度，start、end 的计算是最复杂的，因为可以带 grow()，需要先计算固定长度的，后面再分配动态长度的。</p>
<p>auto style 其实相对好计算，计算完所有管理 view 沿着 axis 的长度后，对比 PinStackView 在 axis 的长度，有变化的话就修改 PinStackView 的长度，然后 callback。说一下 callback，这个通知，可以回调到外部依赖 PinStackView frame 的地方，重新设定相关的 layout。</p>
<p>这个当然有可能造成 layout 的循环，之前的 StackViewLayout 就有这样的场景，我在这里将这个依赖交给了用户，用户自己来控制。</p>
<p>PinStackView 里面对于管理 view 的宽高计算是一个焦点，代码在 calcViewSize() 里面</p>
<ul>
<li>优先 size() 的设置，如果有设置就直接返回</li>
<li>接着调用 view 的 sizeThatFits()，再看 width() 和 hegith() 是否覆盖了 sizeThatFits() 的配置</li>
<li>最后看 width、height 是否超过了外部 padding 后的宽、高</li>
</ul>
<p>将 StackViewLayout 换成 PinStackView 后，循环 layout 的场景不再出现，layout 也挺正常的，但是公司人员变动太大，貌似这个需求测完了，但上线遥遥无期，再说吧。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2021-07.html#p0">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<!-- Page published by cmark-gfm ends here -->
  <div id="foot">2004-<script>var d = new
	Date();document.write(d.getFullYear())</script> &copy;
	Sucha. Powered by MarkdownProjectCompositor.
  </div>
  </div><!-- text -->
  <div id="sidebar">
  </div><!-- sidebar -->
  <script src="../js/prism.min.js" async="async"></script>
  <script src="../js/blog_sidebar.js"></script>
  </div> <!-- body -->
</body>
</html>